9장. 느려도 비싸도 안 돼 (모델 빠르고 싸게 굴리기)

출처: 『AI 엔지니어링』(Chip Huyen 지음, 한국어판) | 원서: AI Engineering (O'Reilly)

코드는 분위기만 — Python·함수 이름 같은 건 몰라도 됩니다. 표의 '비유'와 '위험'만 봐도 충분해요.

이 장은 0장 용어집을 전제로 한다.

특히 추론(쓰는 단계), 지연 시간(답 나오는 시간), 프롬프트(시킬 말), RAG(자료 붙이기), 토큰(글 조각) 은 0장에 풀어 뒀다.

막히면 0장으로 돌아가면 된다.


0. 이 장의 새 단어

0장에 없는 말은 딱 3개다.

각 단어는 [한 문장 뜻 + 일상비유 + 한 줄 예] 3종으로 적었다.


처리량(throughput)

한 문장 뜻 — 1초에 답 글자(토큰)를 몇 개나 뽑아내는지를 재는 양. 많이 뽑을수록 한 번에 더 많은 손님을 받는다.

일상비유 — 식당이 1시간에 손님을 몇 명 먹이느냐. 한 명을 빨리 먹이는 것(지연 시간)과, 시간당 여러 명을 먹이는 것(처리량)은 다른 이야기다. 회전이 빠른 식당이 돈을 더 번다.

한 줄 예 —

# 1초에 몇 토큰을 뽑았나 = 처리량
made = 800   # 8초 동안 800토큰
throughput = made / 8   # 초당 100토큰

KV 캐시(KV cache)

한 문장 뜻 — 모델이 이미 계산해 둔 앞 글자들의 결과를 따로 적어 두고, 다음 글자를 만들 때 다시 안 계산하고 꺼내 쓰는 메모장.

일상비유 — 받아쓰기 메모. 긴 문장을 받아쓸 때, 앞 단어를 매번 처음부터 다시 듣지 않는다. 이미 적어 둔 걸 보고 그 뒤만 이어 쓴다. 그 메모장이 KV 캐시다.

한 줄 예 —

# 앞 글자 계산 결과를 적어 두고 다음 글자에 재사용
cache.save("앞 글자들의 계산 결과")
next_word = model.next(cache)   # 처음부터 다시 안 함

양자화(quantization)

한 문장 뜻 — 모델 속 숫자를 더 거친 숫자로 바꿔, 모델을 가볍게 만들어 빠르고 싸게 굴리는 일.

일상비유 — 사진 화질 낮추기. 원본은 무겁지만, 화질을 조금 낮추면 파일이 절반으로 줄고 열기도 빨라진다. 눈으로 보기엔 거의 똑같다. 모델 숫자도 똑같이 '대충 반올림'해서 가볍게 만든다.

한 줄 예 —

# 정밀한 숫자(3.14159)를 거친 숫자(3.1)로 — 가벼워짐
weight = round(3.14159, 1)   # 3.1, 자리수를 줄임

(귀납 도입) 이런 적 있죠?

답은 똑똑하게 잘 나온다.

그런데 질문 한 번에 5초가 걸린다.

사용자는 화면만 보다가 떠난다.

게다가 사람이 늘자, 월말 청구서가 폭발했다.

모델은 한 번 굴릴 때마다(추론) 시간과 돈을 먹는다.

이걸 몰랐던 것이다.

좋은 답인데 아무도 안 쓴다.

너무 느리고 너무 비싸서다.

여기서 '느림'과 '비쌈'을 줄이는 일이 이 장의 전부다.

이걸 추론 최적화 라고 부른다.

# 답은 좋은데 한 번에 5초 + 매번 돈
answer = model.run(prompt)   # 5초 걸림, 한 번에 0.1달러
# 5초 → 0.5초, 0.1달러 → 0.01달러로 줄이는 게 이 장

한 문장 정의 — 추론 최적화는 답 품질은 그대로 둔 채, 느림(지연 시간)과 비쌈(비용)만 줄이는 일이다.


이 장에서 딱 4가지만

이 장에서 딱 4가지만

  1. 빠르냐는 두 개로 쪼개 잰다. 첫 글자 나올 때까지(TTFT)와, 그 뒤 글자가 줄줄 나오는 속도(TPOT).
  2. 모델을 가볍게. 숫자를 거칠게 바꾸면(양자화) 빨라지고 싸진다.
  3. 두 번 계산하지 않기. 앞 글자 결과를 메모(KV 캐시)해 두고 재사용한다.
  4. 손님을 모아서 한 번에. 여러 질문을 묶어 처리하면(배치) 시간당 처리량이 크게 는다.

각 개념은 아래에서 하나씩 본다.


개념 1 — 빠름은 두 개로 쪼개 잰다 (TTFT와 TPOT)

망가지는 장면

"우리 모델 빠른가요?" 물으면 다들 "느려요" 라고만 한다.

근데 어디가 느린지를 모른다.

첫 글자가 늦게 뜨는 건지, 글자가 뜬 뒤 줄줄 안 나오는 건지 구분을 안 했다.

고칠 데를 못 찾는다.

일상비유

식당에서 음식이 늦다고 느끼는 데는 두 가지가 있다.

하나는 주문하고 첫 접시가 나올 때까지의 시간이다.

다른 하나는 첫 접시 뒤로 나머지가 줄줄 나오는 속도다.

둘은 원인도 다르고 고치는 법도 다르다.

비유 코드 위험
첫 접시까지 걸린 시간 (TTFT) 첫 글자까지의 시간 길면 사용자가 첫 글자 보기 전에 떠남
첫 접시 뒤 나오는 속도 (TPOT) 글자 한 개당 걸리는 시간 느리면 답이 뚝뚝 끊겨 답답함

한 문장 정의 — 지연 시간은 첫 글자까지의 시간(TTFT)그 뒤 글자당 시간(TPOT) 두 개로 쪼개 재며, 어느 쪽이 느린지를 알아야 고칠 데가 보인다.

가장 단순한 규칙 — 느리다는 말이 나오면, 먼저 TTFT인지 TPOT인지부터 가른다.

한 걸음 더 ▸ (지금 몰라도 됨)

시간은 평균으로 보면 속는다. 가끔 한 번 10초 걸린 게 평균을 망친다. 그래서 "느린 순으로 줄 세워 위에서 10%"를 따로 본다. 지금은 "평균만 보면 안 된다" 만 들고 가면 된다.

예시 폭격

예시 1 (worked-example — 완성 예).

전체 답이 나오기까지의 시간을 두 조각으로 나눠 본다.

ttft = 1.0          # 첫 글자까지 1초
tpot = 0.05         # 그 뒤 글자 한 개당 0.05초
n = 100             # 글자 100개
total = ttft + tpot * n   # 1.0 + 5.0 = 6.0초

전체 6초 중, 첫 글자에 1초, 나머지에 5초가 들었다.

예시 2 (before / after).

before — "그냥 느려요" (어디가 느린지 모름, 고칠 수 없음)

after — "TTFT 3초가 길어요" (첫 글자가 늦음 → 첫 글자 쪽을 고치면 됨)

예시 3 (부분 완성 — 빈칸).

같은 답을 첫 글자만 빨리 띄우게 바꿨다. 빈칸을 채워 보자.

ttft = 0.5          # 첫 글자까지 0.5초로 줄임
tpot = 0.05
n = 100
total = ____ + ____ * ____   # 빈칸: ttft, tpot, n
# 답: 0.5 + 0.05 * 100 = 5.5초

예시 4 (독립 적용).

TTFT 2초, TPOT 0.1초, 글자 50개일 때 전체 시간을 직접 구해 보자.

(직접 계산 → 2 + 0.1 × 50 = 7초.)

미니 시나리오

채팅 봇이 "답답하다" 는 항의를 받았다.

재 보니 첫 글자는 0.3초로 빨랐다.

그런데 그 뒤 글자가 한 개당 0.5초로 뚝뚝 끊겼다.

문제는 TTFT가 아니라 TPOT였다.

글자 나오는 속도 쪽을 손봐야 한다.


개념 2 — 모델을 가볍게 (양자화)

망가지는 장면

모델이 너무 무거워서 비싼 장비를 꽉 채운다.

한 번 답하는 데 메모리에서 무거운 숫자를 잔뜩 퍼 올려야 한다.

그래서 느리고, 장비값도 많이 든다.

품질은 좋은데 이걸론 못 팔겠다.

일상비유

고화질 사진을 메신저로 보낸다.

원본은 너무 커서 올라가지도, 받기도 느리다.

화질을 한 단계 낮추면 크기가 절반으로 줄고, 보기에도 거의 똑같다.

모델 속 숫자도 똑같이 '대충 반올림'해서 가볍게 만든다.

이게 양자화다.

비유 코드 위험
화질 낮춰 절반 크기 (양자화함) round(3.14159, 1)3.1 거의 안전 — 너무 거칠게 줄이면 품질이 살짝 떨어짐
원본 그대로 둠 (안 줄임) keep(3.14159265) 무겁고 느리고 비쌈

한 문장 정의 — 양자화는 모델 속 숫자를 더 거친 숫자로 반올림 해 모델을 가볍게 만들어, 빠르고 싸게 굴리는 일이다.

가장 단순한 규칙 — 무거워서 느리면, 먼저 숫자를 한 단계 거칠게(양자화) 줄여 본다. 품질이 견디면 그대로 둔다.

한 걸음 더 ▸ (지금 몰라도 됨)

아무리 줄여도 한계가 있다. 숫자를 너무 거칠게 만들면 답이 망가진다. 지금은 "조금 줄이면 거의 공짜로 빨라진다, 단 끝까지는 못 줄인다" 만 들고 가면 된다.

예시 폭격

예시 1 (worked-example — 완성 예).

무거운 숫자를 거친 숫자로 바꿔 크기를 절반으로 줄인다.

heavy = 3.14159265   # 무거운(정밀한) 숫자
light = round(heavy, 1)   # 3.1, 거칠게 — 메모리 절반

값은 거의 같고, 메모리는 절반이 됐다.

예시 2 (before / after).

before — 정밀한 숫자 그대로. 무겁고 느림.

after — 거친 숫자로 반올림. 가볍고 빠름. 답은 거의 같음.

before = 3.14159265   # 무거움
after  = 3.1          # 가벼움, 결과는 거의 동일

예시 3 (부분 완성 — 빈칸).

숫자 목록을 통째로 한 자리로 줄여 보자. 빈칸을 채운다.

heavy = [3.14159, 2.71828, 1.61803]
light = [round(x, ____) for x in heavy]   # 빈칸: 1
# 답: [3.1, 2.7, 1.6]

예시 4 (독립 적용).

0.57721 을 소수 둘째 자리로 거칠게 줄이면 얼마일까.

(직접 계산 → round(0.57721, 2)0.58.)

미니 시나리오

모델이 무거워 장비 한 대에 겨우 들어간다.

숫자를 한 단계 거칠게 줄였다(양자화).

크기가 절반이 됐다.

같은 장비에 두 배로 올라가고, 속도도 빨라졌다.

답을 몇 개 비교해 보니 품질은 거의 그대로였다.


개념 3 — 두 번 계산하지 않기 (KV 캐시)

망가지는 장면

모델이 답 글자를 하나씩 만든다.

그런데 새 글자를 만들 때마다, 앞에 쓴 글자 전부를 처음부터 다시 계산한다.

답이 길어질수록 같은 계산을 반복하고 또 반복한다.

쓸데없이 느리다.

일상비유

긴 문장을 받아쓰는 사람을 떠올린다.

새 단어를 적을 때마다 첫 단어부터 다시 듣는 사람은 없다.

이미 적어 둔 메모를 보고, 그 뒤만 이어 쓴다.

그 '적어 둔 메모'가 KV 캐시다.

비유 코드 위험
앞 단어는 메모 보고 넘김 (캐시 씀) cache.save(앞 결과) 후 재사용 안전·빠름 — 단 메모가 길어지면 메모장 자리가 큼
매번 처음부터 다시 들음 (캐시 안 씀) recompute(처음부터) 답이 길수록 느려짐 — 같은 계산 반복

한 문장 정의 — KV 캐시는 앞 글자의 계산 결과를 메모해 두고 다음 글자에서 재사용 해, 같은 계산을 두 번 하지 않게 만드는 메모장이다.

가장 단순한 규칙 — 답을 한 글자씩 길게 만들 땐, 앞 결과를 메모(KV 캐시)해 두고 다시 계산하지 않는다.

한 걸음 더 ▸ (지금 몰라도 됨)

메모장도 공짜는 아니다. 답이 아주 길어지면 메모 자체가 자리를 많이 먹는다. 그래서 메모를 더 알뜰하게 쓰는 방법들이 따로 있다. 지금은 "두 번 계산 안 하려고 적어 둔다, 단 적은 게 너무 많아지면 그것도 짐" 만 들고 가면 된다.

예시 폭격

예시 1 (worked-example — 완성 예).

앞 글자 결과를 메모에 쌓아 두고, 다음 글자는 그 뒤만 만든다.

cache = []                  # 메모장
cache.append("첫 글자 결과")   # 적어 둠
cache.append("둘째 글자 결과") # 적어 둠
# 셋째 글자는 cache를 보고 그 뒤만 계산 — 앞은 다시 안 함

예시 2 (before / after).

before — 글자마다 처음부터 다시. 글자 4개면 1+2+3+4 = 10번 계산.

after — 앞은 메모에서 꺼냄. 글자 4개면 1+1+1+1 = 4번 계산.

# before: 매번 처음부터
total_before = 1 + 2 + 3 + 4   # 10번
# after: 앞은 캐시
total_after = 1 + 1 + 1 + 1    # 4번

예시 3 (부분 완성 — 빈칸).

메모장에 새 결과를 적는 줄을 채워 보자.

cache = []
cache.____("새 글자 결과")   # 빈칸: append
# 답: cache.append("새 글자 결과")

예시 4 (독립 적용).

글자 6개를 만들 때, 캐시를 쓰면 계산이 총 몇 번일까.

(직접 계산 → 글자마다 1번씩 → 6번.)

미니 시나리오

긴 답을 만드는데 끝으로 갈수록 점점 느려졌다.

이유는 글자마다 앞 전부를 다시 계산해서였다.

앞 결과를 메모(KV 캐시)에 쌓고 재사용하게 바꿨다.

답이 길어져도 속도가 일정해졌다.


개념 4 — 손님을 모아서 한 번에 (배치)

망가지는 장면

질문이 들어올 때마다 한 건씩 처리한다.

장비는 한 번에 여러 건을 처리할 수 있는데, 한 건만 시키니 자리가 텅텅 빈다.

손님은 줄 서 있는데 식당은 한 명씩만 받는다.

시간당 받는 손님 수(처리량)가 형편없다.

일상비유

버스를 떠올린다.

손님 한 명 올 때마다 버스 한 대를 보내면 낭비다.

자리가 찰 때까지 모았다가 한 번에 태워 보낸다.

질문도 여러 개를 모아 한 번에 처리하면, 같은 시간에 훨씬 많이 처리한다.

비유 코드 위험
여러 명 모아 한 버스 (배치) model.run([q1, q2, q3]) 처리량↑ — 단 모으느라 첫 손님은 살짝 기다림
한 명당 버스 한 대 (한 건씩) model.run(q1) 따로 또 따로 자리 텅 빔 — 시간당 적게 처리, 비쌈

한 문장 정의 — 배치는 여러 질문을 한 묶음으로 모아 한 번에 처리 해, 같은 시간에 더 많이 처리하고(처리량↑) 한 건당 비용을 낮추는 방식이다.

가장 단순한 규칙 — 질문이 많을 땐, 한 건씩 말고 묶어서(배치) 한 번에 굴린다.

한 걸음 더 ▸ (지금 몰라도 됨)

무작정 오래 모으면 첫 손님이 너무 오래 기다린다. 그래서 "끝난 자리에 바로 새 손님을 채우는" 더 똑똑한 방식이 있다. 지금은 "묶으면 처리량이 크게 는다, 단 너무 오래 모으면 첫 손님이 답답해진다" 만 들고 가면 된다.

예시 폭격

예시 1 (worked-example — 완성 예).

세 질문을 한 묶음으로 모아 한 번에 굴린다.

batch = ["질문1", "질문2", "질문3"]   # 한 버스에 셋
answers = model.run(batch)            # 한 번에 처리

한 번 굴려서 답 셋을 받았다.

예시 2 (before / after).

before — 한 건씩 세 번 굴림. 자리 텅 빔. 느리고 비쌈.

after — 셋을 묶어 한 번 굴림. 자리 꽉 참. 시간당 더 많이 처리.

# before: 따로따로 세 번
a1 = model.run("질문1")
a2 = model.run("질문2")
a3 = model.run("질문3")
# after: 한 번에
answers = model.run(["질문1", "질문2", "질문3"])

예시 3 (부분 완성 — 빈칸).

대기 줄에 있는 질문들을 한 묶음으로 만들어 보자.

waiting = ["질문1", "질문2", "질문3", "질문4"]
answers = model.run(____)   # 빈칸: waiting
# 답: model.run(waiting)

예시 4 (독립 적용).

질문 10개를 한 건씩 굴리면 10번, 묶어서 굴리면 몇 번 굴릴까.

(직접 생각 → 한 묶음이면 1번.)

미니 시나리오

요청이 몰리는데 한 건씩 처리하니 줄이 길어졌다.

장비는 여유가 있는데도 한 건만 시키고 있었다.

대기 중인 질문을 묶어서(배치) 한 번에 굴렸다.

같은 장비로 시간당 처리량이 두세 배가 됐다.

청구서도 그만큼 줄었다.


가장 단순한 행동 규칙

느리거나 비싸면, 이 순서로 손댄다.

  1. 먼저 어디가 느린지 가른다. 첫 글자(TTFT)냐, 그 뒤 속도(TPOT)냐.

  2. 무거워서 느리면, 숫자를 한 단계 거칠게 줄인다(양자화).

  3. 답을 길게 만들 땐, 앞 결과를 메모해 두 번 계산을 막는다(KV 캐시).

  4. 질문이 많으면, 묶어서 한 번에 굴린다(배치).

쉬운 것부터, 효과 큰 것부터다.


정리

빠름은 두 개로 쪼개 잰다 — 첫 글자(TTFT)와 그 뒤 속도(TPOT).

가볍게(양자화)·두 번 계산 안 하기(KV 캐시)·묶어 굴리기(배치)로 느림과 비쌈을 줄인다.

품질은 그대로 두고, 느림과 비쌈만 깎는 게 추론 최적화다.

다음 장 예고 1줄. (지금 몰라도 됩니다 — 다음 장에서 이 모든 조각을 하나의 제품으로 합치는 이야기를 합니다.)

난이도
에피소드
질문
카드를 로딩 중...
답변

클릭하거나 Space를 눌러 뒤집기

0 / 0
학습 진도 0%
이동   Space 뒤집기   R 셔플